//===============================================================================
// Microsoft patterns & practices Enterprise Library Contribution
// Common Core Extensions
//===============================================================================

using System;
using System.Data;
using System.Globalization;
using EntLibContrib.Common.Properties;

namespace EntLibContrib.Common
{
	/// <summary>
	/// This class allows a <see cref="DataRow"/> to be read with a datareader-like interface.
	/// Since it implements <see cref="INullableReader"/>, it provides methods to read both
	/// non-null and nullable data fields.
	/// </summary>
	/// <remarks>
	/// This modified version of the code project article by Steve Michelotti, makes use of
	/// the <see langword="default"/> keyword for generic types rather than returning null values.
	/// Original article webLink: http://www.codeproject.com/KB/database/NullableReaders.aspx
	/// </remarks>
	public class NullableDataRowReader : INullableReader
	{
		#region Fields
		private DataRow row;

		/// <summary>
		/// Delegate to be used for value conversion when not null.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="name">column name</param>
		/// <returns>converted value</returns>
		private delegate T Conversion<T>(string name);
		#endregion

		#region Construction
		/// <summary>
		/// prevents the initialization of new instances of the <see cref="T:NullableDataRowReader"/> class without parameters.
		/// </summary>
		private NullableDataRowReader()
		{ }

		/// <summary>
		/// Initializes a new instance of the <see cref="T:NullableDataRowReader"/> class with a given <see cref="T:DataRow"/>.
		/// </summary>
		/// <param name="dataRow">The data row.</param>
		public NullableDataRowReader(DataRow dataRow)
		{
			if (dataRow == null)
				throw new ArgumentNullException("dataRow");

			this.row = dataRow;
		}
		#endregion

		#region INullableReader Members
		/// <summary>
		/// The number of columns in the reader;
		/// </summary>
		public int FieldCount
		{
			get { return row.Table.Columns.Count; }
		}

		/// <summary>
		/// Gets the column name at the ordinal position.
		/// </summary>
		/// <param name="ordinal">The column ordinal.</param>
		/// <returns></returns>
		public string GetName(int ordinal)
		{
			if (ordinal < 0 || ordinal > (FieldCount - 1))
				throw new NullableException(String.Format(CultureInfo.CurrentCulture, InternalResources.OrdinalOutOfRangeException, ordinal.ToString(CultureInfo.CurrentCulture)));

			return row.Table.Columns[ordinal].ColumnName;
		}

		/// <summary>
		/// Gets the ordinal position of the column name.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public int GetOrdinal(string name)
		{
			CheckIsField(name);
			return row.Table.Columns[name].Ordinal;
		}

		/// <summary>
		/// Gets the boolean value.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public bool GetBoolean(string name)
		{
			CheckIsField(name);
			return Convert.ToBoolean(row[name], CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Gets the nullable boolean.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public bool GetNullableBoolean(string name)
		{
			CheckIsField(name);
			return GetNullable<bool>(name, GetBoolean);
		}

		/// <summary>
		/// Gets the byte.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public byte GetByte(string name)
		{
			CheckIsField(name);
			return Convert.ToByte(row[name], CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Gets the nullable byte.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public byte GetNullableByte(string name)
		{
			CheckIsField(name);
			return GetNullable<byte>(name, GetByte);
		}

		/// <summary>
		/// Gets the char.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public char GetChar(string name)
		{
			CheckIsField(name);
			return Convert.ToChar(row[name], CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Gets the nullable char.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public char GetNullableChar(string name)
		{
			CheckIsField(name);
			return GetNullable<char>(name, GetChar);
		}

		/// <summary>
		/// Gets the date time.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public DateTime GetDateTime(string name)
		{
			CheckIsField(name);
			return Convert.ToDateTime(row[name], CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Gets the nullable date time.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public DateTime GetNullableDateTime(string name)
		{
			CheckIsField(name);
			return GetNullable<DateTime>(name, GetDateTime);
		}

		/// <summary>
		/// Gets the decimal.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public decimal GetDecimal(string name)
		{
			CheckIsField(name);
			return Convert.ToDecimal(row[name], CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Gets the nullable decimal.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public decimal GetNullableDecimal(string name)
		{
			CheckIsField(name);
			return GetNullable<decimal>(name, GetDecimal);
		}

		/// <summary>
		/// Gets the double.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public double GetDouble(string name)
		{
			CheckIsField(name);
			return Convert.ToDouble(row[name], CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Gets the nullable double.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public double GetNullableDouble(string name)
		{
			CheckIsField(name);
			return GetNullable<double>(name, GetDouble);
		}

		/// <summary>
		/// Gets the float.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public float GetFloat(string name)
		{
			CheckIsField(name);
			return Convert.ToSingle(row[name], CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Gets the nullable float.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public float GetNullableFloat(string name)
		{
			CheckIsField(name);
			return GetNullable<float>(name, GetFloat);
		}

		/// <summary>
		/// Gets the Guid.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public Guid GetGuid(string name)
		{
			CheckIsField(name);
			if (row[name].GetType() == typeof(string))
				return new Guid(row[name].ToString());
			else
				return (Guid)row[name];
		}

		/// <summary>
		/// Gets the nullable Guid.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public Guid GetNullableGuid(string name)
		{
			CheckIsField(name);
			return GetNullable<Guid>(name, GetGuid);
		}

		/// <summary>
		/// Gets the short integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public short GetInt16(string name)
		{
			CheckIsField(name);
			return Convert.ToInt16(row[name], CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Gets the nullable short integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public short GetNullableInt16(string name)
		{
			CheckIsField(name);
			return GetNullable<short>(name, GetInt16);
		}

		/// <summary>
		/// Gets the integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public int GetInt32(string name)
		{
			CheckIsField(name);
			return Convert.ToInt32(row[name], CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Gets the nullable integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public int GetNullableInt32(string name)
		{
			CheckIsField(name);
			return GetNullable<int>(name, GetInt32);
		}

		/// <summary>
		/// Gets the long integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public long GetInt64(string name)
		{
			CheckIsField(name);
			return Convert.ToInt64(row[name], CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Gets the nullable long integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public long GetNullableInt64(string name)
		{
			CheckIsField(name);
			return GetNullable<long>(name, GetInt64);
		}

		/// <summary>
		/// Gets the string.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public string GetString(string name)
		{
			CheckIsField(name);
			return row[name].ToString();
		}

		/// <summary>
		/// Gets the nullable string.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public string GetNullableString(string name)
		{
			CheckIsField(name);
			if (IsDBNull(name))
				return String.Empty;
			else
				return GetString(name);
		}

		/// <summary>
		/// Gets the time span.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public TimeSpan GetTimeSpan(string name)
		{
			CheckIsField(name);
			if (row[name].GetType() == typeof(TimeSpan))
				return (TimeSpan)row[name];
			else
				return TimeSpan.Parse(row[name].ToString());
		}

		/// <summary>
		/// Gets the nullable time span.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public TimeSpan GetNullableTimeSpan(string name)
		{
			CheckIsField(name);
			return GetNullable<TimeSpan>(name, GetTimeSpan);
		}

		/// <summary>
		/// Gets a data value with conversion to the given type.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="type">The data type.</param>
		/// <returns></returns>
		public object GetValue(string name, Type type)
		{
			CheckIsField(name);
			switch (type.FullName)
			{
				case "System.Boolean":
					return GetBoolean(name);
				case "System.Byte":
					return GetByte(name);
				case "System.Char":
					return GetChar(name);
				case "System.DateTime":
					return GetDateTime(name);
				case "System.Decimal":
					return GetDecimal(name);
				case "System.Double":
					return GetDouble(name);
				case "System.Single":
					return GetFloat(name);
				case "System.Guid":
					return GetGuid(name);
				case "System.Int16":
					return GetInt16(name);
				case "System.Int32":
					return GetInt32(name);
				case "System.Int64":
					return GetInt64(name);
				case "System.String":
					return GetString(name);
				case "System.TimeSpan":
					return GetTimeSpan(name);
				default:
					return GetRawValue(name, type);
			}
		}

		/// <summary>
		/// Gets a data value with conversion to the given type.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="type">The data type.</param>
		/// <returns></returns>
		public object GetNullableValue(string name, Type type)
		{
			CheckIsField(name);
			switch (type.FullName)
			{
				case "System.Boolean":
					return GetNullableBoolean(name);
				case "System.Byte":
					return GetNullableByte(name);
				case "System.Char":
					return GetNullableChar(name);
				case "System.DateTime":
					return GetNullableDateTime(name);
				case "System.Decimal":
					return GetNullableDecimal(name);
				case "System.Double":
					return GetNullableDouble(name);
				case "System.Single":
					return GetNullableFloat(name);
				case "System.Guid":
					return GetNullableGuid(name);
				case "System.Int16":
					return GetNullableInt16(name);
				case "System.Int32":
					return GetNullableInt32(name);
				case "System.Int64":
					return GetNullableInt64(name);
				case "System.String":
					return GetNullableString(name);
				case "System.TimeSpan":
					return GetNullableTimeSpan(name);
				default:
					return GetRawValue(name, type);
			}
		}

		/// <summary>
		/// Gets the type information for the column.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public Type GetFieldType(string name)
		{
			CheckIsField(name);
			return row.Table.Columns[name].DataType;
		}

		/// <summary>
		/// Determines whether the specified name is valid field.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns>
		/// 	<c>true</c> if the specified name is a field; otherwise, <c>false</c>.
		/// </returns>
		public bool IsField(string name)
		{
			return row.Table.Columns.Contains(name);
		}

		/// <summary>
		/// Determines whether the value of the named column is equal to DBNull.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns>
		/// 	<c>true</c> if DBNull; otherwise <c>false</c>.
		/// </returns>
		public bool IsDBNull(string name)
		{
			CheckIsField(name);
			if (row[name].GetType() == typeof(string))
				if (String.IsNullOrEmpty((string)row[name]))
					return true;

			return row[name] == DBNull.Value;
		}

		#endregion

		#region Private Methods
		/// <summary>
		/// This generic method will be called by every interface method in the class.
		/// Using a generic method offers significantly less code and type-safety.
		/// Additionally, the methods can delegate data conversion to an appropriate
		/// method to be executed here.
		/// </summary>
		/// <param name="name">Column name</param>
		/// <param name="convert">Conversion delegate to invoke if the value is not DBNull</param>
		/// <returns>non null converted value</returns>
		/// <typeparam name="T"></typeparam>
		private T GetNullable<T>(string name, Conversion<T> convert) where T : struct
		{
			if (IsDBNull(name))
				return default(T);
			else
				return convert(name);
		}

		/// <summary>
		/// Gets the raw data value checking for DBNull in the process
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="type">The data type.</param>
		/// <returns></returns>
		private object GetRawValue(string name, Type type)
		{
			if (IsDBNull(name))
			{
				if (type.IsValueType)
					throw new NullableException(InternalResources.NullNotSupportedException);
				else
					return null;
			}
			else
				return row[name];
		}

		/// <summary>
		/// Checks if the field name is valid and throws exception if not.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <exception cref="IndexOutOfRangeException">The field name has not been found as a column of the <see cref="DataRow"/></exception>
		private void CheckIsField(string name)
		{
			if (!IsField(name))
				throw new NullableException(String.Format(CultureInfo.CurrentCulture, InternalResources.ColumnMissingException, name));
		}
		#endregion
	}
}
